#include "inorder/l1cache.h"
#include "math.h"

namespace config
{
    const Value<uint64_t> l1cache_outstanding_num = {"l1cache_outstanding_num", 1, "l1cache outstanding num"};
    const Value<uint64_t> l1cache_mshr_num = {"l1cache_mshr_num", 1, "l1cache mshr num"};
    const Value<uint64_t> l1cache_sbuf_size = {"l1cache_sbuf_size", 2, "l1cache store buffer size"};
    const Value<uint64_t> l1cache_size = {"l1cache_size", 4096, "l1cache data bytes"};
    const Value<uint64_t> l1cache_line_size = {"l1cache_line_size", 8, "l1cache line bytes"};
    const Value<uint64_t> l1cache_associativity = {"l1cache_associativity", 8, "l1cache associativity"};
    const Switch l1cache_enable_clock_gating = {"l1cache_enable_clock_gating", "l1cache enable clock gating enable"};

} // namespace config

L1Cache::L1Cache() : load_hit_cnt("L1Cache-LoadHit"), load_miss_cnt("L1Cache-LoadMiss"), store_hit_cnt("L1Cache-StoreHit"), store_miss_cnt("L1Cache-StoreMiss"), gated_cycle("L1Cache-GatedCycle")
{
    m_data_size = config::l1cache_size;
    m_line_size = config::l1cache_line_size;
    m_assoc_way = config::l1cache_associativity;

    m_index_size = m_data_size / m_line_size / m_assoc_way;

    m_offset_bits = log2(m_line_size);
    m_index_bits = log2(m_index_size);
    m_tag_bits = sizeof(word_t) * 8 - m_index_bits - m_offset_bits;

    m_offset_padding = 0;
    m_index_padding = m_offset_bits;
    m_tag_padding = m_index_padding + m_index_bits;

    m_offset_mask = (1ul << m_offset_bits) - 1ul;
    m_index_mask = ((1ul << m_index_bits) - 1ul) << m_index_padding;
    m_tag_mask = ((1ul << m_tag_bits) - 1ul) << m_tag_padding;

    metas = new Meta *[m_assoc_way];
    for (int i = 0; i < m_assoc_way; i++)
    {
        metas[i] = new Meta[m_index_size];
    }

    replacer = new RandomReplacer(m_assoc_way);
    reset();

    // std::cout << "[Cache] m_offset_bits " << std::to_string(m_offset_bits)
    // << " m_index_bits " << std::to_string(m_index_bits)
    // << " m_tag_bits " << std::to_string(m_tag_bits) << std::endl;

    // std::cout << " m_offset_padding " << m_offset_padding
    //           << " m_index_padding " << m_index_padding
    //           << " m_tag_padding " << m_tag_padding << std::endl;

    // std::bitset<64> m_offset_mask_bit(m_offset_mask);
    // std::bitset<64> m_set_mask_bit(m_index_mask);
    // std::bitset<64> m_tag_mask_bit(m_tag_mask);
    // std::cout << "[L1Cache] m_offset_mask_bit " << m_offset_mask_bit.to_string() <<
    // " m_set_mask_bit " << m_set_mask_bit.to_string() <<
    // " m_tag_mask_bit " << m_tag_mask_bit.to_string() << std::endl;
}
L1Cache::~L1Cache()
{
    for (int i = 0; i < m_assoc_way; i++)
    {
        delete metas[i];
    }
    delete metas;
}
void L1Cache::reset()
{
    store_buffer.clear();
    mshrs.clear();
    resp.clear();
    while (req.size())
    {
        req.pop();
    }

    for (int i = 0; i < m_assoc_way; i++)
    {
        for (int j = 0; j < m_index_size; j++)
        {
            metas[i][j].state = CacheLineState::None;
        }
    }
    s1 = none_latch;
    s2 = none_latch;
}
bool L1Cache::send(OP *op)
{
    if (req.size() >= config::l1cache_outstanding_num)
        return false;
    req.push(op);
    return true;
}
bool L1Cache::recv(OP *op)
{
    auto it = std::find_if(resp.begin(), resp.end(), [op](OP *resp_op)
                           { return op->id == resp_op->id; });

    if (it == resp.end())
    {
        return false;
    }
    resp.erase(it);
    return true;
}
void L1Cache::run()
{
    state.mem_port_busy = false;
    state.meta_port_busy = false;
    bool clock_gated = config::l1cache_enable_clock_gating && state.in_release == false && mshrs.size() == 0 && store_buffer.size() == 0 && req.size() == 0 && s1.op == NULL && s2.op == NULL;
    if (clock_gated)
    {
        gated_cycle++;
        return;
    }
    Stage3();
    Stage2();
    Stage1();
}

bool L1Cache::hazard_check(OP *op)
{
    for (auto store : store_buffer)
    {
        if ((store->trace.mem_access_addr & ~m_offset_mask) == (op->trace.mem_access_addr & ~m_offset_mask))
            return true;
    }
    for (auto mshr : mshrs)
    {
        auto mshrop = std::get<0>(mshr);
        if ((mshrop->trace.mem_access_addr & ~m_offset_mask) == (op->trace.mem_access_addr & ~m_offset_mask))
            return true;
    }
    return false;
}

bool L1Cache::bypass_check(OP *op)
{
    return false;
}

std::tuple<bool, int, L1Cache::Meta> L1Cache::access_meta(word_t addr)
{
    if (state.meta_port_busy)
    {
        SPDLOG_ERROR("[L1Cache] Meta port access conflict");
    }
    state.meta_port_busy = true;
    bool hit = false;
    word_t index = (addr & m_index_mask) >> m_index_padding;
    word_t tag = (addr & m_tag_mask) >> m_tag_padding;
    int hit_way = -1;
    L1Cache::Meta hit_meta = {0};
    for (int i = 0; i < m_assoc_way; i++)
    {
        auto meta = metas[i][index];
        if (meta.state != CacheLineState::None && meta.tag == tag)
        {
            SPDLOG_TRACE("addr{:016x} tag{:016x} mask{:016x}", addr, meta.tag, m_tag_mask);
            hit = true;
            hit_way = i;
            hit_meta = meta;
            break;
        }
    }
    return std::make_tuple(hit, hit_way, hit_meta);
}
void L1Cache::Stage1()
{
    if (state.meta_port_busy)
    {
        SPDLOG_TRACE("[L1Cache] S1 stall by meta port busy");
        return;
    }
    bool stall = false;
    if (req.size() == 0)
    {
        stall = true;
    }
    else if (s1.op != NULL)
    {
        stall = true;
        SPDLOG_TRACE("[L1Cache] S1 stall by S2 not ready.");
    }
    else if (hazard_check(req.front()))
    {
        SPDLOG_TRACE("[L1Cache] S1 stall by Hazard Check true.");
        stall = true;
    }

    if (stall)
    {
        if (req.size())
        {
            SPDLOG_TRACE("store_buffer {} mshrs {}", store_buffer.size(), mshrs.size());
        }
        if (store_buffer.size() == 0)
        {
            return;
        }
        SPDLOG_TRACE("[L1Cache] S1 StoreBuffer Refill.");

        auto refill_op = store_buffer.front();
        store_buffer.pop_front();

        bool hit = false;
        word_t addr = refill_op->trace.mem_access_addr;
        word_t index = (addr & m_index_mask) >> m_index_padding;
        word_t tag = (addr & m_tag_mask) >> m_tag_padding;
        for (int i = 0; i < m_assoc_way; i++)
        {
            auto meta = metas[i][index];
            if (meta.state != CacheLineState::None && meta.tag == tag)
            {
                hit = true;
                meta.state = CacheLineState::Dirty;
                break;
            }
        }
        if (hit == false)
        {
            SPDLOG_ERROR("[L1Cache] S1 StoreBuffer Miss.");
        }
        assert(hit);
        return;
    }

    // if (hazard_check(req.front()))
    // {
    //     SPDLOG_TRACE("[L1Cache] S1 stall by Hazard Check true.");
    //     return;
    // }

    s1.op = req.front();
    req.pop();

    const auto [hit, hit_way, hit_meta] = access_meta(s1.op->trace.mem_access_addr);
    SPDLOG_TRACE("[L1Cache] S1 receive op {} : hit {}.", s1.op->id, hit);
    if (s1.op->trace.opcode == OP_LD)
    {
        if (hit)
            load_hit_cnt++;
        else
            load_miss_cnt++;
    }
    else if (s1.op->trace.opcode == OP_ST)
    {
        if (hit)
            store_hit_cnt++;
        else
            store_miss_cnt++;
    }
#if COUNT_MEMORY_ACCESS_SPACE
    if (s1.op->trace.mmio_access == false)
    {
        word_t addr = s1.op->trace.mem_access_addr;
        addr -= 0x80000000ull;
        word_t index = addr / m_line_size;
        if (index < 8192)
        {
            if (s1.op->trace.opcode == OP_LD)
            {
                m_space_ld[index]++;
                if (m_space_ld[index] == 1)
                {
                    m_space_ld_cnt++;
                }
                if (index > m_space_ld_max)
                    m_space_ld_max = index;
            }
            else
            {
                m_space_st[index]++;
                if (m_space_st[index] == 1)
                {
                    m_space_st_cnt++;
                }
                if (index > m_space_st_max)
                    m_space_st_max = index;
            }
        }
    }
#endif
    s1.hit = hit;
    s1.hit_way = hit_way;
    s1.hit_meta = hit_meta;
    s1.miss = !hit;
}

void L1Cache::Stage2()
{
    if (s1.op == NULL)
        return;
    if (s2.op != NULL)
        return;
    if (s1.miss && mshrs.size() < config::l1cache_mshr_num)
    {
        if (state.mem_port_busy)
            return;
        state.mem_port_busy = true;
        if (s1.dram_op == NULL)
        {
            s1.dram_op = new DramOP(s1.op->trace.mem_access_addr, m_line_size, false);
        }

        bool success = dram->send(s1.dram_op); // TODO: Change the Load/Store TraceOP to Load MemoryOP
        SPDLOG_TRACE("[L1Cache] S2 {} send op to mshrs", success ? "success" : "failed");
        if (success)
        {
            mshrs.push_back(std::make_tuple(s1.op, MSHR_State::RecvLD, s1.dram_op));
            s2 = s1;
            s1 = none_latch;
        }
        return;
    }
    if (s1.hit)
    {
        if (s1.op->trace.opcode == OP_ST)
        {
            if (store_buffer.size() < config::l1cache_sbuf_size)
            {
                store_buffer.push_back(s1.op);
                s2 = s1;
                s1 = none_latch;
            }
            return;
        }
        else if (s1.op->trace.opcode == OP_LD)
        {
            s2 = s1;
            s1 = none_latch;
            return;
        }
        else
        {
            std::cerr << "[L1Cache] Error. Could not process non-memory op.\n";
        }
    }
    return;
}
void L1Cache::Stage3()
{

    bool release_done = false;

    if (state.in_release)
    {
        SPDLOG_TRACE("[L1Cache] S3 op {} in release", state.release_op->id);
        if (state.release_meta.state != CacheLineState::Dirty)
        {
            release_done = true;
        }
        else
        {
            if (state.release_wait_ack)
            {
                bool success = dram->recv(state.release_dram_op);
                if (success)
                {
                    state.release_dram_op = NULL;
                    state.release_wait_ack = false;
                    release_done = true;
                }
            }
            else
            {
                if (state.mem_port_busy)
                    return;
                state.mem_port_busy = true;
                if (state.release_dram_op == NULL)
                {
                    state.release_dram_op = new DramOP((state.release_tag << m_tag_padding), m_line_size, true);
                }
                bool success = dram->send(state.release_dram_op);
                if (success)
                {
                    state.release_wait_ack = true;
                }
            }
        }
    }

    for (auto it = mshrs.begin(); it != mshrs.end();)
    {
        auto &[mshr_op, mshr_state, mshr_dram_op] = *it;

        if (mshr_state == MSHR_State::RecvLD && !state.in_release)
        {
            bool success = dram->recv(mshr_dram_op);
            SPDLOG_TRACE("[L1Cache] S3 MSHR op {} dram_op {} recv {}", mshr_op->id, mshr_dram_op->id, success ? "success" : "failed");
            if (success)
            {
                delete mshr_dram_op;
                if (mshr_op->trace.opcode == OP_LD)
                {
                    resp.push_back(mshr_op);
                    it = mshrs.erase(it);
                }
                else if (mshr_op->trace.opcode == OP_ST)
                {
                    store_buffer.push_back(mshr_op);
                    resp.push_back(mshr_op);
                    it = mshrs.erase(it);
                }
                else
                {
                    SPDLOG_ERROR("[L1Cache] MSHR Should not process non memory op");
                }

                word_t addr = mshr_op->trace.mem_access_addr;
                word_t index = (addr & m_index_mask) >> m_index_padding;
                word_t tag = (addr & m_tag_mask) >> m_tag_padding;
                int victim_way = replacer->replace(addr);

                state.meta_port_busy = true;
                state.release_meta = metas[victim_way][index];

                state.release_wait_ack = false;
                state.victim_way = victim_way;
                state.release_tag = tag;
                state.release_index = index;
                state.in_release = true;
                state.release_op = mshr_op;
                state.release_dram_op = NULL;
                continue;
            }
        }
        ++it;
    }
    if (release_done)
    {
        SPDLOG_TRACE("[L1Cache] S3 op {} release done", state.release_op->id);
        state.meta_port_busy = true;
        metas[state.victim_way][state.release_index] = {.tag = state.release_tag, .state = CacheLineState::Valid};
        state.in_release = false;
    }

    if (s2.op == NULL)
        return;
    if (s2.op->trace.opcode == OP_LD)
    {
        if (s2.hit)
        {
            resp.push_back(s2.op);
            s2 = none_latch;
            return;
        }
        if (s2.miss)
        {
            s2 = none_latch;
            return;
        }
    }
    else if (s2.op->trace.opcode == OP_ST)
    {
        if (s2.hit)
        {
            resp.push_back(s2.op);
        }
        s2 = none_latch;
        return;
    }
    else
    {
        std::cerr << "[L1Cache] Error. Could not process non-memory op.\n";
    }
}

void L1Cache::print_status()
{
    printf(COLOR("[L1Cache-MSHR] ", GREEN) "size:%ld {", mshrs.size());
    for (auto mshr : mshrs)
    {
        auto op = std::get<0>(mshr);
        printf("%ld ", op->id);
    }
    printf("}\n");

    printf(COLOR("[L1Cache-StoreBuffer] ", GREEN) "size:%ld {", store_buffer.size());
    for (auto sbuf : store_buffer)
    {
        printf("%ld ", sbuf->id);
    }
    printf("}\n");
}

void L1Cache::print_counter()
{
    auto total_load = load_hit_cnt + load_miss_cnt;
    auto total_store = store_hit_cnt + store_miss_cnt;

    if (total_load)
    {
        float load_hit_rate = load_hit_cnt * 100.0f / (total_load);
        printf(COLOR("[L1Cache] ", GREEN) "Load  \thit rate : %.03f%% miss rate : %.03f%%\n", load_hit_rate, 100 - load_hit_rate);
    }
    if (total_store)
    {
        float store_hit_rate = store_hit_cnt * 100.0f / (total_store);
        printf(COLOR("[L1Cache] ", GREEN) "Store \thit rate : %.03f%% miss rate : %.03f%%\n", store_hit_rate, 100 - store_hit_rate);
    }
#if COUNT_MEMORY_ACCESS_SPACE
    printf(COLOR("[L1Cache] ", GREEN) "m_space_ld_max:%d m_space_st_max:%d\n", m_space_ld_max, m_space_st_max);
    printf(COLOR("[L1Cache] ", GREEN) "m_space_ld_cnt:%d m_space_st_cnt:%d\n", m_space_ld_cnt, m_space_st_cnt);
    printf("m_space_ld:\n");
    for (size_t i = 0; i < m_space_ld_max; i++)
    {
        printf("%d,", m_space_ld[i]);
    }
    printf("\n");
    printf("m_space_st:\n");
    for (size_t i = 0; i < m_space_st_max; i++)
    {
        printf("%d,", m_space_st[i]);
    }
    printf("\n");
#endif
}